Deno v2.1で導入されたOpenTelemetryサポートを試してみる
#OpenTelemetry #Deno #@deno/otel #@opentelemetry/api #@opentelemetry/sdk-trace-base #@opentelemetry/sdk-metrics
はじめに
Deno Advent Calendar 2024 20日目の記事です🎅🎄
Deno v2.1でOpenTelemetryのサポートが実験的に導入されたので、試してみました
⚠️ DenoのOpenTelemetryのサポートはまだ安定化はされていないため、今後、破壊的変更が導入される可能性もあります
⚠️ 筆者はOpenTelemetryに詳しくなく、ところどころ一般的な規約やベストプラクティスなどに従えていない箇所があるかもしれません🙏
目的
Deno v2.1で入ったOpenTelemetryサポートに関する紹介
deno fmtのYAMLサポートに関する紹介
バージョン
Deno@2.1.4+9d315f2 (※⚠️ Deno v2.1.5もしくはv2.2.0あたりで導入されると思われる変更に依存しているため、canaryバージョンを使って検証しています)
OpenTelemetry Collector@0.114.0
@deno/otel@0.0.2
@opentelemetry/api@1.9.0
@opentelemetry/sdk-metrics@1.29.0
要約
⚠️実験的APIのため、今後、使用方法などが変更される可能性があります
1. Denoの実行時にOTEL_DENO=trueと--unstable-otelを指定することで、fetchやDeno.serveなどのAPIの計装やconsole.*によるLogsの送信が有効化されます
2. カスタムのTracesやMetricsを送信したいときは、Deno公式の@deno/otelパッケージと@opentelemetry/apiや@opentelemetry/sdk-metricsを併用するとよさそうです
3. 現在、開発が進められているFresh v2向けにもサポートが進められています
追記) 2025/01/12
Deno v2.1.5から@opentelemetry/apiとの連携に@deno/otelの使用が不要になりました (OTEL_DENO=trueと--unstable-otelが設定されていれば、自動で連携できます)
code:javascript
import { trace } from "npm:@opentelemetry/api@^1.9.0";
const tracer = trace.getTracer("example");
function someOperation() {
return new Promise((ok) => setTimeout(ok, 3000));
}
await tracer.startActiveSpan("someOperation", async (span) => {
await someOperation();
span.end();
});
※ただし、Deno v2.1.5で試したところ、startActiveSpanの実行でエラーが起きてしまうようです
追記) この問題はDeno v2.2.3で修正されました ()
1. OpenTelemetry Collectorの設定をする
Configuration | OpenTelemetryを参考に設定を用意してみます (参考: 執筆時点での最新のドキュメント)
OpenTelemetry Collectorの設定ファイルを用意します
code:otelcol.yaml
receivers:
otlp:
protocols:
http:
endpoint: localhost:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
metrics:
receivers: otlp
exporters: debug
traces:
receivers: otlp
exporters: debug
ひとまず疎通の確認をしたいのでdebugエクスポーターを有効化しておきます
Denoの内部で使われているopentelemetry-rustではデフォルトでhttp://localhost:4318向けにSignalsを送信するようなので、それに合わせてエンドポイントの設定をしておきます (opentelemetry-otlp/src/exporter/mod.rs)
deno fmtコマンドはYAMLもサポートしているので、設定ファイルをフォーマットできて便利です
code:shell
$ deno fmt otelcol.yaml
下記コマンドで設定内容を確認できます
code:shell
$ otelcol validate --config=otelcol.yaml
OpenTelemetry Collectorを起動します
code:shell
$ otelcol --config=otelcol.yaml
2. 依存パッケージのダウンロード
必要に応じて依存パッケージをダウンロードします
code:shell
# 1) カスタムのTracesを送信したい場合
$ deno add jsr:@deno/otel npm:@opentelemetry/api
# 2) カスタムのMetricsを送信したい場合
$ deno add npm:@opentelemetry/sdk-metrics
3. Tracesの送信
現状、Denoの内部ではfetchとDeno.serveの2つのAPIで計装が行われており、これらのAPIを使うと自動的にバックエンドへTracesが送信されるようです
fetch APIを使ってTracesの送信を試してみます
code:main.js
await fetch("https://api.github.com/repos/denoland/deno", {
headers: {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
--unstable-otel及びOTEL_DENO=true環境変数を指定して上記スクリプトを実行してみます
code:shell
$ OTEL_DENO=true deno run --allow-env --allow-net --unstable-otel main.js
OpenTelemetry Collectorに以下のようなログが出力されます
code:shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0
Resource SchemaURL:
Resource attributes:
-> process.runtime.version: Str(2.1.4+9d315f2)
-> telemetry.sdk.name: Str(deno-opentelemetry)
-> telemetry.sdk.language: Str(deno-rust)
-> service.name: Str(unknown_service)
-> process.runtime.name: Str(deno)
-> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope deno 2.1.4+9d315f2
Span #0
Trace ID : 828595b8b4a8f6699bd1ec38a47e1eed
Parent ID :
ID : 21c40683a9c261de
Name : GET
Kind : Client
...
Status code : Unset
Status message :
Attributes:
-> http.request.method: Str(GET)
-> url.full: Str(https://api.github.com/repos/denoland/deno)
-> url.scheme: Str(https)
-> url.path: Str(/repos/denoland/deno)
-> url.query: Str()
-> http.response.status_code: Str(200)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
Deno本体で計装されたAPIからのtracesの送信を無効化したい場合、OTEL_DENO_TRACING=falseによって無効化できそうです (cli/args/flags.rs)
4. Logsの送信
console.*で出力したログは自動的にLogsとしてバックエンドへ送信されます
code:main.js
console.info("foo");
このスクリプトを実行してみます
code:shell
$ OTEL_DENO=true deno run --allow-env --unstable-otel main.js
foo
すると、OpenTelemetry Collectorで以下のようなログが出力されます
code:shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceLog #0
Resource SchemaURL:
Resource attributes:
-> telemetry.sdk.name: Str(deno-opentelemetry)
-> process.runtime.name: Str(deno)
-> process.runtime.version: Str(2.1.4+9d315f2)
-> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0)
-> telemetry.sdk.language: Str(deno-rust)
-> service.name: Str(unknown_service)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope deno
LogRecord #0
...
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(foo
)
Trace ID:
Span ID:
Flags: 0
{"kind": "exporter", "data_type": "logs", "name": "debug"}
Logsの送信に関する振る舞いはOTEL_DENO_CONSOLE環境変数によって調整できるようです (cli/args/flags.rs)
例) OTEL_DENO_CONSOLE=ignoreを指定すると、バックエンドへのLogsの送信が無効化されます
5. カスタムのTracesの送信
Deno本体のテストコードを参考にカスタムのTracesを送信してみます ( tests/specs/cli/otel_basic/basic.ts)
@deno/otelと@opentelemetry/apiを併用することで送信できます
code:javascript
import { trace } from "@opentelemetry/api";
import { register } from "@deno/otel";
register();
const tracer = trace.getTracer("example");
function someOperation() {
return new Promise((ok) => setTimeout(ok, 3000));
}
await tracer.startActiveSpan("someOperation", async (span) => {
await someOperation();
span.end();
});
OpenTelemetry CollectorのログからsomeOperationという名前のSpanが作成されていることを確認できます
code:shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0
Resource SchemaURL:
Resource attributes:
-> process.runtime.version: Str(2.1.4+9d315f2)
-> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0)
-> process.runtime.name: Str(deno)
-> service.name: Str(unknown_service)
-> telemetry.sdk.name: Str(deno-opentelemetry)
-> telemetry.sdk.language: Str(deno-rust)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope example
Span #0
Trace ID : 771ee0a3832c8040b04e2a883516240c
Parent ID :
ID : f289545ad405c65e
Name : someOperation
Kind : Internal
...
Status code : Unset
Status message :
{"kind": "exporter", "data_type": "traces", "name": "debug"}
fetchとの併用
自前で作成したSpan内でDeno内部で計装されているAPI (fetch)を実行した場合、適切に親子関係の紐づけが行われるようです
code:javascript
import { trace } from "@opentelemetry/api";
import { register } from "@deno/otel";
register();
const tracer = trace.getTracer("example");
await tracer.startActiveSpan("makeHTTPRequest", async (span) => {
await fetch("https://api.github.com/repos/denoland/deno", {
headers: {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
span.end();
});
fetchに対応するSpanにParent IDが設定されており、makeHTTPRequestが参照されています
code:shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceSpans #0
Resource SchemaURL:
Resource attributes:
-> process.runtime.name: Str(deno)
-> telemetry.sdk.version: Str(2.1.4+9d315f2-0.27.0)
-> telemetry.sdk.name: Str(deno-opentelemetry)
-> process.runtime.version: Str(2.1.4+9d315f2)
-> telemetry.sdk.language: Str(deno-rust)
-> service.name: Str(unknown_service)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope deno 2.1.4+9d315f2
Span #0
Trace ID : 2a709f3c26099083d36ce9be197c7fd8
Parent ID : f99d09c34da6d3cd
ID : bf8ef320a2e9024a
Name : GET
Kind : Client
...
Status code : Unset
Status message :
Attributes:
-> http.request.method: Str(GET)
-> url.full: Str(https://api.github.com/repos/denoland/deno)
-> url.scheme: Str(https)
-> url.path: Str(/repos/denoland/deno)
-> url.query: Str()
-> http.response.status_code: Str(200)
ScopeSpans #1
ScopeSpans SchemaURL:
InstrumentationScope example
Span #0
Trace ID : 2a709f3c26099083d36ce9be197c7fd8
Parent ID :
ID : f99d09c34da6d3cd
Name : makeHTTPRequest
Kind : Internal
...
Status code : Unset
Status message :
{"kind": "exporter", "data_type": "traces", "name": "debug"}
6. カスタムMetricsの送信
Deno本体のテストコードを参考に、カスタムMetricsを送信してみます (tests/specs/cli/otel_basic/metric.ts)
@opentelemetry/sdk-metricsとDeno.telemetry.MetricExporterを併用すると送信できるようです
code:javascript
import {
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
const exporter = new Deno.telemetry.MetricExporter();
const meterProvider = new MeterProvider();
meterProvider.addMetricReader(
new PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: 1000,
}),
);
const meter = meterProvider.getMeter("meter");
const views = meter.createCounter("views", {
description: "The total number of views of the page",
});
views.add(1);
await new Promise((ok) => setTimeout(ok, 1500));
views.add(1);
await new Promise((ok) => setTimeout(ok, 1500));
OpenTelemetry Collectorの出力内容)
code:shell
YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0
Resource SchemaURL:
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope meter
Metric #0
Descriptor:
-> Name: views
-> Description: The total number of views of the page
-> Unit:
-> DataType: Sum
-> IsMonotonic: true
-> AggregationTemporality: Cumulative
NumberDataPoints #0
...
Value: 1.000000
{"kind": "exporter", "data_type": "metrics", "name": "debug"}
YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0
Resource SchemaURL:
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope meter
Metric #0
Descriptor:
-> Name: views
-> Description: The total number of views of the page
-> Unit:
-> DataType: Sum
-> IsMonotonic: true
-> AggregationTemporality: Cumulative
NumberDataPoints #0
...
...
Value: 2.000000
{"kind": "exporter", "data_type": "metrics", "name": "debug"}
YYYY-MM-DDTHH:mm:ss.SSSZZ info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 1, "data points": 1}
YYYY-MM-DDTHH:mm:ss.SSSZZ info ResourceMetrics #0
Resource SchemaURL:
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope meter
Metric #0
Descriptor:
-> Name: views
-> Description: The total number of views of the page
-> Unit:
-> DataType: Sum
-> IsMonotonic: true
-> AggregationTemporality: Cumulative
NumberDataPoints #0
...
Value: 2.000000
{"kind": "exporter", "data_type": "metrics", "name": "debug"}
7. Fresh v2でのサポートについて
⚠️ Fresh v2はまだアルファバージョンであり、正式リリースは行われていません
Deno本体でのOpenTelemetryサポートを活用して、Fresh v2向けに計装が進められているようです (feat: add open telemetry instrumentation (denoland/fresh#2786))
おわりに
OpenTelemetryのサポートはまだ実験的なので今後どうなるかはわからないのですが、もしかしたらDeno Deployの方にもサポートが入る可能性があるかもしれないので、結構便利な機能なのではないかと思いました
参考
@opentelemetry/api
open-telemetry/opentelemetry-js/doc/sdk-registration.md
open-telemetry/opentelemetry-js/doc/tracing.md
@opentelemetry/sdk-metrics
関連ページ
DenoのOpenTelemetryサポートについて